Skip to content

chore: merge current upstream dev into welcome gutter successor - #15

Open
twoimo wants to merge 1082 commits into
successor/welcome-composer-gutter-cleanfrom
tmp/upstream-dev-10a32cfd
Open

chore: merge current upstream dev into welcome gutter successor#15
twoimo wants to merge 1082 commits into
successor/welcome-composer-gutter-cleanfrom
tmp/upstream-dev-10a32cfd

Conversation

@twoimo

@twoimo twoimo commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Internal synchronization PR. Merge current upstream dev into the preserved welcome/composer gutter successor without squashing feature history.

Yeachan-Heo and others added 30 commits July 21, 2026 13:33
Regenerate the public schema description so it reflects the compatibility-only fallback behavior already declared by the source settings schema.

Constraint: Generated schema verification is required on the exact latest dev baseline
Rejected: Hand-maintain divergent schema text | would fail the source-of-truth gate
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Regenerate schemas whenever settings descriptions change
Tested: v0.11.6/dev schema gate, URL guard tests, and 19-PR integration
Not-tested: Hosted CI on the new PR head
* fix(session): migrate legacy local root before resume

* fix(session): handle absent managed artifact root

* test(session): allow managed resume setup time
…d display (Yeachan-Heo#2794)

Fable 5 adaptive thinking was billed but never displayed. Model discovery
parsed claude-fable-5 as an unknown family and cached mode 'budget',
sending enabled+budget_tokens that Fable answers with signature-only
thinking; and supportsAdaptiveThinkingDisplay only matched Opus 4.7+, so
the display: summarized opt-in never applied to Fable on either the
Anthropic Messages or Bedrock Converse transport.

Fixes Yeachan-Heo#2791
…an-Heo#2808)

* fix(coding-agent): reject prose image paths

* fix(coding-agent): reject multiline singular pasted-image paths

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…ollback and replaying the transcript (Yeachan-Heo#2796)

iTerm2 delivers SIGWINCH-driven resize events on tab activation and
window focus changes without an actual grid size change. The resize
handler forced a redraw on every event; on hosts still using the
fullRender path (legacy multiplexer opt-in, non-process terminals) that
clears scrollback (2J/H/3J) and replays the entire transcript, parking
the native viewport at the transcript top — users returning to the tab
found the thread scrolled to the very beginning. On viewport-repaint
hosts it forced a pointless full-viewport repaint.

requestResizeRender() now forces only when the terminal dimensions
actually changed since the last committed frame; same-size events fall
through to a plain diff render (a no-op when nothing changed). Genuine
resizes, viewport-repaint hosts, and the explicit forced-redraw paths
(Ctrl+Z resume, external editor) are unaffected.

Regression test proves the unguarded path emitted 2J/H/3J plus a full
60-line transcript replay on a same-size resize event.
…#2815)

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…Yeachan-Heo#2813)

* fix(ai): degrade a non-base64 Anthropic image payload to text

A resident image externalized to a content-addressed blob is referenced by a
`blob:sha256:` sentinel. When the blob goes missing, session materialization
bakes a human-readable placeholder into the image content block's `data`
(`{type:"image", data:"[Session resident imageData blob missing: …]", mimeType}`).

`convertContentBlocks` forwarded that `data` straight into the Anthropic wire
block `source.data`, so the request carried a non-base64 image payload and the
API rejected the whole request:

    400 invalid_request_error: messages.N…image.source.base64: invalid base64 data

Because the poisoned image lives in the persisted tool_result, every subsequent
turn re-sends it and fails identically — the session is bricked, even for a
plain text message.

Guard the wire format: an image whose `data` is not standard base64 is degraded
to a text block (its placeholder is preserved so the model still sees the
context) instead of being sent as an image. Valid base64 images are unchanged.

Adds regression tests: a missing-blob placeholder image degrades to text (no
`source`, no invalid base64), and a valid base64 image is preserved as an image
block.

* fix(ai): strict base64 validation before emitting an Anthropic image

Address review on the previous head: the alphabet-only check accepted malformed
payloads (`"a"`, `"abcde"`, misplaced/overlong padding, `length % 4 === 1`), which
would still be emitted as `source.data` and rejected by the API with the same
400 `invalid base64 data`.

Validate standard (RFC 4648) base64 structurally — correct quartet grouping and
trailing `=`/`==` padding — accepting both canonical padded and unpadded forms,
and rejecting whitespace, data URLs, URL-safe (`-`/`_`) alphabets, misplaced or
overlong padding, `length % 4 === 1`, prose, and empty input. The pattern has no
nested quantifier, so oversized inputs are rejected in linear time. Valid images
keep their block order and MIME; only non-base64 payloads degrade to text.

Expands tests to cover valid padded/unpadded/oversized payloads and
degradation of whitespace, data URLs, URL-safe forms, malformed padding/length,
prose, empty, and oversized inputs, plus block-order/MIME preservation — all via
the production `convertAnthropicMessages` converter.

* style(ai): format base64 degradation regression

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
* fix(session): verify Windows ACLs before startup repair

* chore(telegram): refresh native authority attestations

* fix(telegram): advance daemon generation for ACL authority

* fix(chat): advance daemon generations for ACL authority

* fix(natives): satisfy native authority checks

* chore(telegram): refresh final native digest

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…Heo#2810)

* docs(models): declare vision input for custom OpenAI-compatible models

Custom models that only set `id` default to input: [text], so GJC strips
images with "[image omitted: model does not support vision]" even when the
upstream host (e.g. Aliyun Token Plan qwen3.8-max-preview) supports vision.
Document the required input: [text, image] declaration and lock the default
behavior with focused ModelRegistry tests.

* chore(docs): regenerate docs index for models vision note

docs/models.md changed; check:public-sync requires docs-index.generated.ts
to match. Fixes Public site sync CI on Yeachan-Heo#2810.
Map immutable Anthropic cache-eval evidence to its focused affected-path test.
…Yeachan-Heo#2821)

formatBytes clamped the terminal GB unit below the next-unit boundary that
only KB/MB need, so every value >= 1 TiB rendered as "1023.9GB" (e.g. 2 TiB
-> "1023.9GB" instead of "2048.0GB"). formatDuration's seconds branch used
toFixed(1), which rounds any ms in [59950, 59999] up to "60.0s" instead of
staying below the minute unit.

Skip the clamp on the terminal GB unit and route the seconds branch through
the existing roundBelow helper (already used by formatNumber for this exact
invariant). Add regression tests covering both boundaries and the preserved
sub-boundary behavior.
…#2831)

* fix(notifications): tailor uncertain lifecycle guidance

- Pass the originating lifecycle verb into Telegram outcome formatting.
- Keep duplicate-start warnings on create while describing close and resume uncertainty accurately.
- Add command-specific regression coverage and an Unreleased changelog entry.

Tests:
- bun test packages/coding-agent/test/notifications-lifecycle-commands.test.ts packages/coding-agent/test/notifications-lifecycle-command-routing.test.ts packages/coding-agent/test/notifications-lifecycle-orchestrator.test.ts
- bunx biome check targeted files
- git diff upstream/dev...HEAD --check

* docs(changelog): reuse unreleased fixed section
* feat: add ephemeral retained multi-turn /btw-r side chat

Keep one-shot /btw unchanged and add /btw-r as a same-panel temporary
thread that reuses the BTW controller/panel path without writing the
main session transcript. Plain editor text becomes follow-ups while
open, slash commands still dispatch normally, tools stay off, and Esc
closes the retained panel.

* fix: keep failed /btw-r turns in retained context and satisfy biome

Failed retained turns now append the user question plus an error
assistant message into contextMessages so follow-ups like "try again"
still see the failed question. Also fix biome optional-chain/format
issues that broke CI path validation.

* fix: keep slash skills on follow-up path while /btw-r is open

Mirror Enter routing in handleFollowUp so slash-origin text (including
/skill:*) is not captured into the retained side thread. Adds a regression
test for the follow-up keybinding path.

* fix: satisfy typecheck for retained slash-skill routing test

Cast the harness context through unknown when stubbing skillCommands and
promptCustomMessage so package check:types accepts the regression test.

* fix: capture non-slash /btw-r text before inline skill parse

While a retained /btw-r thread is open, plain follow-ups that merely
mention an embedded /skill: token must stay in the side chat. Move the
non-slash retained capture ahead of #invokeSkillCommand and add an Enter
path regression test.

* fix: isolate btw-r retained content flow

* refactor: make btw multi-turn by default

* fix: enforce btw privacy lifecycle boundaries

* fix: close btw privacy review gaps

* chore: retrigger successor PR checks

---------

Co-authored-by: project820 <239489026+project820@users.noreply.github.com>
…eachan-Heo#2760)

DeepSeek V4 (accessed through OpenRouter) rejects `strict: true` on
function tool definitions with a 400 error even when the parameter
schema is valid JSON Schema draft 2020-12.

Scope the fix to the canonical compatibility boundary: in
detectOpenAICompat, suppress supportsStrictMode when the model
family is DeepSeek AND the provider is OpenRouter. This keeps
strict mode enabled for:
  - non-DeepSeek models via OpenRouter (Claude, GPT, etc.)
  - DeepSeek direct API (api.deepseek.com)
  - DeepSeek via other providers (NVIDIA NIM, etc.)

Only the OpenRouter + DeepSeek combination is affected.
…-2835-btw-test-contracts

fix: route notification BTW through side-turn contract
…an-Heo#2755)

The compiled stats server previously extracted its embedded dashboard bundle into a predictable cross-process filesystem location. Parse validated archive entries into an in-memory Blob map instead, serve assets directly from that map, and build the embedded archive without a temporary archive file.

Constraint: Exact gzip bytes remain mtime-dependent, which is a pre-existing generator property; verification targets archive content and canonical reset cleanliness
Rejected: Harden the shared extraction directory | a cross-process filesystem boundary would remain in the compiled request path
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep compiled dashboard requests backed by validated in-memory assets and do not reintroduce shared extraction paths
Tested: focused compiled-client-assets tests (4 pass); all stats tests (37 pass); stats Biome and both TypeScript configs; public sync; generator generate/reset with exact placeholder restoration; git diff --cached --check
Not-tested: End-to-end HTTP smoke through a compiled dashboard binary
Fixes Yeachan-Heo#2812. Exact head `377ff98351eff85777b7a78cbf5a0df19af40578` passed 25/25 hosted checks and independent CLEAR safety review.

—
*[repo owner's gaebal-gajae (clawdbot) 🦞]*
…legacy-resume-oom

# Conflicts:
#	packages/coding-agent/test/notifications-telegram-btw-e2e.test.ts
#	packages/coding-agent/test/resume-confirm-continue.test.ts
…-2834-legacy-resume-oom

fix: reduce managed legacy-resume memory amplification
Yeachan-Heo and others added 29 commits July 25, 2026 00:03
…suites (Yeachan-Heo#3082)

Six telegram test suites merged on dev (Yeachan-Heo#2984 / Yeachan-Heo#2958 / Yeachan-Heo#2959) without
regenerating test/manifests/telegram-baseline-v1.json, so the exact-match
gate in check:sdk-closure fails on pristine dev for everyone. Regenerated
via bun scripts/generate-telegram-baseline-manifest.ts (55 commands).
Co-authored-by: jasonyooyj <47925973+jasonyooyj@users.noreply.github.com>
* fix(tui): expose provider retry recovery state (Yeachan-Heo#3071)

* fix(tui): repair provider retry recovery state

* fix(tui): harden retry rendering edge cases

* fix(tui): reject cloned retry replay events

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…et (Yeachan-Heo#3068)

* fix(coding-agent): isolate tombstone reconciliation failures per-target

reconcileManagedTombstones() iterated every tombstone in a scope with no
per-target error boundary: one target throwing (e.g. identity_mismatch on
a stale artifact directory) propagated out of the loop entirely, failing
prepareManagedSessionScopeForWrite for the whole scope and blocking
resume/delete of every other, unrelated session in that scope.

This function is documented as best-effort GC of already-retired sessions
("without restoring retired candidates"), so a single unresolvable
tombstone should not be able to block access to live sessions. Wrap each
target's reconciliation in a try/catch that leaves the tombstone for a
future attempt instead of aborting the whole scope.

Reproduced on Windows 11 / GJC 0.11.8: a session whose delete got stuck on
an identity_mismatch during artifact detach left every other session in
the same managed scope unable to resume or delete.

* docs(changelog): note tombstone scope isolation fix

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(session): cover tombstone isolation session-identity gate

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(changelog): drop unfilled issue-number placeholder

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(tui): collapse and redact bash invocations (Yeachan-Heo#3072)

* fix(tui): harden bash invocation rendering

* fix(tui): redact interactive bash headers

* fix(tui): close shell redaction bypasses

* fix(tui): redact escaped shell invocations

* fix(tui): preserve nested shell substitution spans

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…erations epoch, marker-free heartbeat, root GC, 429 cooldown, lazy topics (gen 27) (Yeachan-Heo#3048)

* feat(notifications): servingEpoch serve-across-generations compatibility + fail-closed legacy gen-absent row (WS2/D4)

* fix(notifications): epoch-gated reload requires signalable provenance; preserve dead legacy reclaim; epoch-era test fixtures

* fix(notifications): cooldown attach requires signalable ready provenance (TOCTOU)

* test(notifications): route malformed servingEpoch fixtures through an explicit unknown boundary

* fix(notifications): renew daemon ownership through fenced sidecar

* test(notifications): cover steady sidecar heartbeat freshness

* fix(notifications): route daemon liveness through sidecar freshness

* test(notifications): post-TTL sidecar acceptance and steady-renewal zero-growth coverage

* fix(notifications): corrupt or unreadable heartbeat sidecar degrades to state-floor freshness

* fix(notifications): harden daemon heartbeat ownership

* test(notifications): stale-rename-after-steal window stays inert and successor self-heals

* fix(notifications): release managed telegram roots on teardown

* fix(notifications): honor Telegram flood-control cooldowns

* test(notifications): cover Telegram flood cooldown

* test(sdk): cover setup root isolation and corrupt markers

* test(notifications): transiently unreadable root is kept and does not block live-root scanning

* fix(telegram): handle cooldown suppression and root cleanup

* test(telegram): cover root cleanup lifecycle retries

* chore(telegram): bump guarded daemon generation

* test(telegram): cover generation convergence harnesses

* fix(telegram): fail closed during reload cooldown

* test(telegram): bridge lifecycle pollers in D6

* test: align telegram daemon control fixtures

* style: apply biome formatting to telegram structural-fix surfaces

* test(notifications): tolerate platform-dependent clock-read counts in the cooldown successor test

* fix(telegram-daemon): resolve six Yeachan-Heo#3048 review blockers

(1) replace banned ReturnType<> with exported OwnerFreshnessSnapshot;
(2) joinExclusive uses Promise.withResolvers instead of raw new Promise;
(3) cooldown-suppressed flush grants requeue untouched and settle
ambiguous (retry after cooldown), and mid-item suppression flips an
otherwise-accepted grant to ambiguous via a suppressed-call counter;
(4) readiness gates on serving-epoch compatibility with a safe-integer
generation check, attaching same-epoch cross-generation owners;
(5) a failed initial heartbeat-sidecar proof after ready publication
rolls the state forward to retired and exact-unlinks the rebound
ownership lock;
(6) per-registration tokens fence root unregister/rollback/prune
end-to-end so a stale same-session cleanup never deletes a live
replacement registration, with legacy token-less registrations keeping
root-match behavior; also fix an optional-call short-circuit that
skipped registration when no onRegistered callback was provided.

Add adversarial regression tests for each blocker, refresh the
telegram-daemon generation guard manifest and the telegram baseline
test manifest, and record the fix in the changelog.

* fix(telegram-daemon): close adversarial review blockers

* fix(telegram): fence removal and classify per-call outcomes

* fix(telegram): fence last-root daemon stop

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…Yeachan-Heo#3109)

* feat(agents): give read-only role agents irc and read-only git access

`architect`, `planner`, and `critic` pin an explicit `tools:` allow-list, so
they only receive the tools they name. That excluded `irc`, and their
`bashAllowedPrefixes` allowed only `gjc ralplan --write` / `gjc state`, so
every git command was blocked.

- add `irc` to the three agents' tool lists (`executor` and generic subagents
  declare no `tools:` frontmatter and already inherit `irc` from the builtin
  set, so they are intentionally untouched)
- add read-only git prefixes: status, log, show, diff, blame, rev-parse,
  ls-files. These agents use the workflow bash profile, where
  `validateMatchedGjcCommand` passes any non-`gjc` command once a prefix
  matches, so mutating git (commit/push/reset/checkout/branch -D/config) and
  arbitrary shell stay blocked and the read-only contract holds.
- sync AGENTS.md and the shared restricted-bash prompt fragment, which both
  documented the old `gjc`-only contract
- add a behavioral regression test driving the real bundled agent
  definitions through `checkBashAllowedPrefixes`, rather than only pinning
  the prefix array

`web_search` was already present in all three lists; it is now asserted.

* fix(task): keep irc active for subagents instead of hiding it behind discovery

`irc` is `loadMode: "discoverable"`, and `tools.discoveryMode` defaults to
`"all"`, so the initial-tool filter in `createAgentSession` dropped it from
every subagent that does not pin an explicit `tools:` list — notably
`executor`. The tool was constructed and reachable, but only after the model
spent a `search_tool_bm25` round-trip to find it, which is the wrong tradeoff
for a coordination channel the agent needs proactively.

- add `CreateAgentSessionOptions.alwaysActiveToolNames`: discoverable built-ins
  that stay in the initial active set even under `discoveryMode: "all"`
- pass `["irc"]` from the subagent executor when the parent runtime reports
  IRC is actually available (`ircAvailable`), so behavior still follows the
  existing `irc.enabled` + peer-roster gating
- cover it with a session-level regression test asserting irc is hidden by
  default for a subagent-style session, active when forced, and that forcing
  one tool does not drag in the rest of the discoverable set

Read-only role agents are unaffected: they pass explicit tool names, so irc
already survived the filter via the explicitly-requested path.

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…h race (Yeachan-Heo#3111)

* test: remediate 11 confirmed-flaky tests and 1 superseded member; fix broker identity publish race

Phase 2 of the flaky-CI stabilization program (audit-scoped, approval-gated):

- sdk/broker/identity.ts: atomic identity publication via same-directory
  temp file + hard link with EEXIST reread (real product race reproduced
  under 4x parallel contention: "Invalid broker identity key")
- sdk-broker.test.ts: new concurrent identity-publication regression test
- 11 flaky-test rewrites fixing adjudicated root causes (deterministic
  awaits over fixed sleeps, fixture isolation, lease/heartbeat seams,
  bounded polling, mkdtemp AuthStorage isolation, scoped timeouts)
- session-directory.test.ts: remove one superseded append-only-sidecar
  member (superseded by retained-cleanup contract, c49c259)

Evidence chain: 62-suspect audit (2-week window, 3 reconciliation
identities, blinded second validation, third-adjudicator protocol),
24 verification receipts, mutation-probe matrix, 220/220 stress +
80/80 contention iterations. No workflow/branch-protection changes;
dev PR CI critical path untouched.

* style: apply biome formatting to Phase 2 remediation test edits

Formatter-only (line wrapping); no logic change. Fixes the
check:@gajae-code/coding-agent CI failure on this branch.

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…sh (Yeachan-Heo#3115)

Follow-up to Yeachan-Heo#3109, which gave `architect`/`planner`/`critic` read-only git
but had to document a workaround: `git diff HEAD~1` was rejected and had to be
quoted as `git diff 'HEAD~1'` (or rewritten as `HEAD^` / an explicit SHA).

The restricted-bash parser treated `~` as an unsafe expansion character
anywhere in a command. Bash only performs tilde expansion when the tilde opens
a word, so `HEAD~1` is a literal argument and the rejection was a false
positive against ordinary git revision syntax.

`~` is now checked positionally instead of unconditionally: word-initial
tildes (`~`, `~/path`, `~user`) are still rejected because bash really does
expand them, while a tilde inside a word is allowed. Every other expansion
character (`$`, `*`, `?`, `[`, `]`, `{`, `}`), command substitution, control
operators, and backslash escapes are unchanged.

Verified against real bash: `HEAD~1`, `HEAD~2`, `HEAD~1..HEAD`, `HEAD~5`, and
`main...HEAD` all print literally, while `~`, `~/secrets`, and `~root` expand
to home directories and therefore stay blocked.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…an-Heo#3117) (Yeachan-Heo#3121)

* fix(security): reject bash tilde expansion in assignment words (Yeachan-Heo#3117)

Follow-up repair to Yeachan-Heo#3115, which made the restricted role-agent bash parser
treat `~` positionally but only rejected it at the start of a whitespace-
delimited word. Bash also performs tilde expansion inside an assignment word:
directly after the first `=` and after each `:` in the assigned value, so
`A=~`, `foo=~root/bar`, `A=x:~`, and `a=x:~:y:~` all expanded while the parser
accepted them.

`parseShellWords` now tracks per-token assignment state and rejects an unquoted
`~` at every position bash can expand it: word start, right after the first
unquoted `=` of an assignment word, and right after an unquoted `:` in that
word's value. An assignment word requires the raw characters before the first
unquoted `=` to match `[A-Za-z_][A-Za-z0-9_]*` with no quoting, so
non-assignment tokens (`--opt=~`, `a-b=~`, `1abc=~`, `=~`) and non-expansion
positions (`a=x~y`, `a=x:y~z`, `a=b=~`) stay allowed, as do literal mid-word git
revisions such as `HEAD~1`. Token state resets at whitespace boundaries.

Verified against real bash: `A=~`, `A=x:~` expand to home paths while
`a=b=~`, `a=x~y`, and `HEAD~1` stay literal. Parameter expansion, command
substitution, control operators, newlines, backslash escapes, and the tilde
denial reason string are unchanged.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(security): recognize compound `+=` assignment words in restricted bash (Yeachan-Heo#3117)

Adversarial review of Yeachan-Heo#3121 found a fail-open bypass: bash(1) defines an
assignment word as both `name=value` and `name+=value`, and both tilde-expand
in the value, but `ASSIGNMENT_NAME_PATTERN` was tested against the region
before the first unquoted `=`, which holds `A+` for `A+=`. `A+` failed the
name pattern, so `tildeExpandable` was never armed and `A+=~`, `A+=x:~`, and
`A+=~root/bar` were allowed through despite real bash expanding all three.

The assignment-name pattern now accepts an optional single trailing `+`, so a
compound assignment word arms tilde expansion on exactly the same positions as
a plain one, including the `:` continuation in the value. Nothing else changes:
`a++=~`, `+=~`, and `a+b=~` still fail name validation and stay allowed, as do
`a+=x~y` and `a+=b=~` where bash does not expand.

Verified against bash 5.1.16 with HOME=/HOMEDIR: `A+=~` and `A+=x:~` expand,
while `a+=x~y`, `a+=b=~`, `a++=~`, `+=~`, and `a+b=~` stay literal. The denial
reason string is unchanged.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…eo#3122)

* ci: add non-required rehearsal dispatch mode to release CI

The stabilization plan requires proving the release graph is green without
waiting for a real tag (tags are non-deterministic and must not gate a soak).
Every ci.yml job is already switched on `startsWith(github.ref, 'refs/tags/v')`,
so a workflow_dispatch rehearsal mode reuses the EXACT production job
definitions in place — no reusable-workflow extraction, no drift risk, and
job IDs / check context names are unchanged.

- `workflow_dispatch` input `rehearsal`: tag-build-verify | main-nontag
- tag-graph jobs (native, binaries) additionally run under
  dispatch:tag-build-verify; non-tag jobs under dispatch:main-nontag
- publish stays tag-only AND is explicitly excluded from dispatch
  (`github.event_name != 'workflow_dispatch'`), so NPM_TOKEN — referenced
  only inside publish, with no `secrets: inherit` anywhere — is unreachable
  from any rehearsal
- concurrency group gains a dispatch-mode suffix so a rehearsal can never
  cancel a real release run

Behavior for push-main, pull_request, and push-tag is unchanged: the added
clauses only constrain workflow_dispatch. Release-policy and publish-order
guard tests updated accordingly (35 tests green).

* test(ci): expect test:@gajae-code/stats in the native-change push plan

packages/stats has a `test` script and a real test/ directory, so the
planner correctly emits test:@gajae-code/stats for full push plans. The
hardcoded expectation in the native-workspace planning test was stale and
fails on clean dev — this PR's workflow edit merely routes the selftest
selector into the plan, which surfaced it.

Verified: fails identically at origin/dev without this change.

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…view state (Yeachan-Heo#3127)

Two defects made the deep-interview workflow unusable in practice.

**Guard blocked `/dev/null` redirects.** `extractBashTargets` captured every
`>`/`>>` target, so `cmd 2>/dev/null` was treated as a repository write and
blocked during any planning phase — including the brownfield exploration
deep-interview itself mandates before Round 1.

Only `/dev/null` is exempted. `/dev/stdout`, `/dev/stderr`, and `/dev/fd/<n>`
stay blocked: they are descriptor aliases, and `exec 1<>src/product.ts;
printf x >/dev/stdout` reaches a real file through a rebound descriptor.

Suppressing a sink target must not turn a mutating command into an
empty-target "safe" verdict, so this also closes the bypasses that suppression
would otherwise open:
- `exec` redirections fail closed (descriptor rebinding is not statically resolvable)
- `>|`, `>&path`, and `<>` write forms are recognized
- path-qualified writers (`/bin/dd`, `/usr/bin/tee`) are matched
- every `dd of=` operand is inspected, not just the first (GNU dd honors the last)
- a redirect capture that dequotes to nothing is `unknown`, not safe

**Hook-seeded state failed native validation.** The draft CLI ran
`validateDeepInterviewV1Envelope` whenever `verifyWorkflowEnvelopeReceiptValue`
returned `native-valid`, but that verdict covers receipt shape and checksum
only — it does not imply a native v1 body. The hook's minimal `ModeState` has
no `schema_version` and no `state`, so every typed operation failed with
`DI_STATE_SCHEMA_INVALID` and interviews ran fully manually with `rounds: []`.

Gate on `isNativeDeepInterviewV1` instead, matching the already-correct
`transformGuardedWorkflowEnvelopeAtomic`, and seed a native v1 envelope from
the hook. The seed omits `initial_idea`: pre-seeding `""` made a later
`initialize-context` carrying a real idea fail with `DI_SETUP_CONFLICT`.

Round shells now canonicalize the agent-supplied `deepInterview.dimension`
label, which is free text on post-topology asks but must be a canonical id in
persisted state.

.gjc/** writes remain blocked in every case.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
* Keep ACP eval execution inside the established permission boundary

Eval-triggered JavaScript and Python tool dispatch must pass through the same prepared session tool path as model-issued calls, so ACP policy remains authoritative while recursion and lifecycle ownership stay centralized. This preserves the accepted implementation direction from the closed contribution chain without unrelated repairs.

Constraint: Fresh replacement is based exactly on upstream/dev e3c7f6a.

Constraint: Publication remains gated on terminal-success exact-dev CI plus explicit owner authorization.

Rejected: Add a second eval-specific authorization layer | it would duplicate policy and risk disagreement with the canonical session boundary.

Confidence: high

Scope-risk: narrow

Reversibility: clean

Directive: Keep nested eval tool dispatch routed through getToolForExecution; raw registry lookup would bypass ACP session policy.

Tested: Exact six-file ACP/SDK/Python integration set (98 tests, 488 assertions); coding-agent Biome and TypeScript checks; operation inventory generation and --check; native addon build; production binary build; root/source and compiled-binary CLI smokes; isolated compiled-binary and source-linked install smokes; git diff, stable patch-id, and merge-tree checks.

Not-tested: Hosted-only Linux x64 tarball install substep requiring the CI-provided pi_natives.linux-x64 addon.

Related: Yeachan-Heo#2737, Yeachan-Heo#2980, Yeachan-Heo#2992

* docs(changelog): move ACP eval permission entry into Unreleased

The Yeachan-Heo#2737 sweep note required the changelog entry to land in the current
Unreleased/Fixed section. At head 0ae2315 it was appended to the already
released [0.11.8] block instead, retroactively editing a shipped release
section. Moved verbatim into [Unreleased] > Fixed; no other change.

Co-authored-by: Oreochococukie <Oreochococukie@users.noreply.github.com>

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
Co-authored-by: Oreochococukie <Oreochococukie@users.noreply.github.com>
The workflow-level `permissions: contents: write` grant was redundant:
`publish` is the only job that needs write and already declares its own
job-level `contents: write` override. The workflow-level grant handed a
write-scoped GITHUB_TOKEN to every build/verify job, including the new
rehearsal dispatches, which only check out and build.

No job outside `publish` references GITHUB_TOKEN, github.token, `gh`, or
`git push`, so narrowing the default is behavior-neutral for real
push/PR/tag runs while removing unnecessary write scope from rehearsals.

Surfaced by an independent audit of the stabilization program.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…ancellation/timeout regression (successor to Yeachan-Heo#3112) (Yeachan-Heo#3131)

* fix(vendor/insane-search): P0 — 429 rate-limit no longer kills fetch + bias_check violations

Problem 1: RATE_LIMITED was in TERMINAL_NONSUCCESS, causing a single 429
on the probe or any grid candidate to immediately terminate the entire
fetch pipeline — no grid diversity, no browser fallback, no backoff.

Fix:
- Remove RATE_LIMITED from TERMINAL_NONSUCCESS (validators.py)
- Add _rate_limit_backoff() with linear escalation (2s→10s cap) and
  Retry-After header support (fetch_chain.py)
- Grid now skips rate-limited candidates and continues to the next
  TLS/referer combo instead of breaking
- Browser fallback is no longer skipped on 429

Problem 2: bias_check CI gate reported violations for WAF vendor domains
(funcaptcha.com) and Jina Reader (r.jina.ai) used as infrastructure.

Fix:
- Add funcaptcha.com, api.funcaptcha.com, r.jina.ai, jina.ai to
  URL_ALLOWLIST in bias_check.py with explanatory comments

Verified:
- bias_check → clean
- TERMINAL_NONSUCCESS == {auth_required, not_found} (429 excluded)

* fix(vendor/insane-search): repair two NameErrors in SessionPool.warmup + record local patches

engine/transport.py SessionPool.warmup() referenced two names that do not
exist in its scope:

  1. allow_private  — computed inline into classify_url() but never bound,
                      then passed to _fetch_following() on the next line.
  2. DEFAULT_MAX_REDIRECTS — lives in engine.safety; the sibling call site
                      at get_or_create() already qualifies it as
                      safety.DEFAULT_MAX_REDIRECTS.

Every warmup() call that passed the safety gate raised NameError, so the
per-(host, impersonate) root warmup — the step that lets a WAF sensor set
a resolved session cookie before the deep request — never actually ran.
engine/tests/test_u4.py failed on the vendored tree because of this.

Also records all three local fixes in MANIFEST.json localPatches, per the
vendoring convention, so a future re-vendor from upstream does not
silently drop them.

Verified:
- engine/tests: 8/8 files pass (was 7/8)
- engine/bias_check.py: clean
- bun run ci:check:full: exit 0

* fix(vendor/insane-search): pass resp to _rate_limit_backoff on all 429 paths + probe backoff + tests

Review findings addressed (PR Yeachan-Heo#3104):

1. _rate_limit_backoff(resp) now receives the actual response on every 429
   path (grid AND probe), so server-provided Retry-After is read and honoured
   (numeric, capped at 30s). Previously resp was discarded.

2. Probe-phase 429 now triggers backoff before entering the grid, preventing
   the first grid candidate from hitting the same rate window.

3. Deterministic regression tests (engine/tests/test_rate_limit_backoff.py):
   - Retry-After header read + cap at 30s
   - Probe-phase 429 backoff
   - Grid-phase 429 passes resp
   - Repeated 429s bounded by attempt budget, terminate correctly
   - 429 does not defeat Playwright browser fallback
   - Linear escalation (base*1..base*5 cap)
   All sleeps mocked; no real delays.

4. transport.py: SECURITY NOTE clarifies the SSRF guard is resolver-level
   (pre-connect) only — NOT transport-bound DNS validation. Does not claim
   equivalence to DNS rebinding or redirect-to-private authority protection.

5. MANIFEST.json localPatches entry updated to reflect the Retry-After fix
   and test coverage.

* fix(vendor/insane-search): clamp/validate rate-limit backoff base + cancellation/timeout regression

Addresses REQUEST_CHANGES on Yeachan-Heo#3112 (bounded/cancellable backoff contract).

P0: _rl_base = float(INSANE_RATE_LIMIT_BACKOFF_S) was neither validated nor
capped. Verified failure modes: a non-numeric value raised ValueError inside
_fetch_core; NaN produced an undefined time.sleep; inf hung time.sleep forever
(defeating any timeout); a negative value raised ValueError in time.sleep; a
huge value bypassed the advertised 30s bound (the cap only applied to the
Retry-After header, not the base-derived delay).

Fix:
- Add fetch_chain._clamp_rate_limit_base(): a total parser that rejects
  non-numeric/NaN/infinite/negative values (falls back to the 2s default) and
  clamps a valid value to the 30s ceiling (_RATE_LIMIT_MAX_DELAY).
- Hard-cap the final per-attempt delay to _RATE_LIMIT_MAX_DELAY with a
  finite/non-negative guard before time.sleep, so the bound holds end-to-end
  even for a huge-but-finite base, and sleeps stay short enough that an
  abort/KeyboardInterrupt between attempts is honoured promptly (cancellable).

Tests (test_rate_limit_backoff.py):
- The header claimed abort/timeout coverage but contained none. Fixed the
  header and added the missing deterministic regressions:
  * cancellation: KeyboardInterrupt raised during a backoff sleep propagates
    immediately and cuts the chain short (backoff does not swallow the abort);
  * timeout: no single sleep exceeds 30s or becomes non-finite even under a
    huge base AND a huge Retry-After, so a per-attempt deadline can rely on it;
  * base-validation: helper unit test + end-to-end huge/negative/NaN/inf/
    non-numeric env cases.
- All new tests fail/error against the unfixed code and pass with the fix
  (14 tests total, all deterministic, mocked sleep).

Provenance: kept the MANIFEST.json local-patch entry and documented the
hardening accurately.

Boundary: the disabled production TypeScript bridge is intentionally untouched
— DNS authority is not transport-bound, so that boundary stays closed.

Gates: test_rate_limit_backoff.py (14), test_hardening.py (4), bias_check.py
(clean), scripts/verify-insane-vendor.ts (passed).

---------

Co-authored-by: dmae97 <dmae97@users.noreply.github.com>
Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…4 migration before publishing successor identity (Yeachan-Heo#3080)

Gate the successor `'/tmp/gjc-local/019f97df-38e7-7000-a304-bd5a7ea8e374'` root before the successor session identity is published, across `/new` (both lease branches), `fork()`, handoff, `/resume` switchSession, and branch/tree-jump.

Adversarial exact-head review approved at review #4778681080 for head a1878d7.

Verification at merge time:
- Per-site rotation -> gate -> publication ordering confirmed at all six sites.
- Exhaustive adjudication of the full #beginSessionTransition set: clear-context, navigate-tree, and compact perform no identity rotation and correctly need no gate.
- Order-inversion mutation testing proved the added invariant test is non-vacuous (fails with markerExists=false when the gate is moved after identity publication on the covered path).
- Focused suites 13/13 and 66/66 pass, deterministic across 3 runs; coding-agent typecheck clean.
- Exact-head CI green on a1878d7 (15 success, 5 conditional skips, 0 failures).
- Current dev da7cc11 CI terminal success (24 success, 5 skipped, 0 failures).
- merge-tree against current dev: no conflicts; dev's two intervening commits touch disjoint files.

Non-blocking follow-ups noted in review: the pre-existing cleanup_pending sync/async marker asymmetry in internal-urls (untouched by this PR), and order-inversion regression coverage for the five gated sites the new test does not exercise.
* feat(models): repoint opus presets to claude-opus-5

Anthropic shipped claude-opus-5 (2026-07-24) with the same published
envelope as claude-opus-4-8: 1M context, 128k output, effort range
low..max, 5/25 pricing. Repoint the opus-related built-in presets to it.

- claude-opus: default/planner/critic/architect -> anthropic/claude-opus-5
  with existing effort suffixes; executor stays anthropic/claude-sonnet-5
- opus-codex: default -> anthropic/claude-opus-5:xhigh; the durable
  anthropic/claude-sonnet-5 planner override is unchanged
- fable-opus-codex: planner/critic -> anthropic/claude-opus-5; the fable
  default and codex roles are unchanged
- Regenerate packages/ai/src/models.json so anthropic/claude-opus-5
  resolves; zero providers or models removed
- Extend the generator's Claude Opus vision normalization to opus-5,
  which upstream shipped without image input on three variants, and
  replace the duplicated substring guard with a shared exact-generation
  parser plus a tripwire test that fails when the catalog bundles an
  Opus generation newer than any reviewed one
- Realign preset expectation tests and update docs; measured opus-4-8
  figures in docs/multi-vendor-profiles.md stay attributed to opus-4-8
  rather than silently transferring to an unmeasured model

* fix(models): remove unrelated opus preset catalog drift

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.